'use strict'

class AudioPlayer {
    #APP_NAME = "Audio Player";
    #playlist = [];
    #currentOrderNum = 0;
    #audioContext = null;
    #source = null;
    #gainCtrl = null;
    #infoBar = null;
    #forceStop = false;
    #animationId = null;
    #canvas = null;
    #mirror = null;
    #listContainer = null;
    #disableControl = null;
    #enableControl = null;

    constructor(infoBar, playBtn, canvas, mirror, listContainer, disableControl, enableControl) {
        this.#infoBar = infoBar;
        this.#canvas = canvas;
        this.#mirror = mirror;
        this.#listContainer = listContainer;
        this.#disableControl = disableControl;
        this.#enableControl = enableControl;
        try {
            this.#addVolumeListener();
            this.#audioContext = new AudioContext();
            console.info("audioContext init state:", this.#audioContext.state);
            this.#audioContext.onstatechange = () => {
                if (this.#audioContext.state === "running") {
                    playBtn.textContent = '||';
                    playBtn.title = 'pause';
                } else {
                    playBtn.textContent = '>';
                    playBtn.title = 'play';
                }
                console.info("audioContext state:", this.#audioContext.state);
            };
        } catch (e) {
            this.#updateTitle('!Your browser does not support AudioContext');
            console.error(e);
        }
    }

    addToList(song) {
        this.#playlist.push(song);
        if (this.#playlist.length == 1) {
            // init item added, auto play
            this.play();
        }
    }

    emptyList() {
        this.#playlist = [];
        this.stop();
        this.#savePlayList();
    }

    removeFromList(orderNum, targetEle) {
        var item = this.#playlist.splice(orderNum, 1); //remove the specified item from the list
        if (item[0].file == null) { // item in db removed, it is a valid play list item
            this.#savePlayList();
        }
        this.#listContainer.removeChild(targetEle);
        if (orderNum < this.#currentOrderNum) {
            this.#currentOrderNum -= 1;
            return;
        };
        if (this.#playlist.length === 0) {
            //list is empty, reset all variables
            emptyList();
            return;
        };
        if (orderNum === this.#currentOrderNum) {
            this.playNext();
        }
    }

    togglePlay() {
        if (this.#audioContext.state === "running") {
            this.#audioContext.suspend();
        } else if (this.#audioContext.state === "suspended") {
            if (this.#source) {
                this.#audioContext.resume();
            } else {
                this.play();
            }
        } else {
            console.info("audio state is", this.#audioContext.state);
        }
    }

    play(orderNum) {
        if (this.#playlist.length === 0) {
            return;
        }
        if (orderNum != null) {
            console.info("force play:", orderNum);
            this.#currentOrderNum = orderNum;
        } else {
            console.info("auto play");
        }
        var lis = this.#listContainer.getElementsByTagName('li');
        for (var i = 0; i < lis.length; i++) {
            if (i === this.#currentOrderNum) {
                lis[i].className='current';
            } else {
                lis[i].className='';
            }
        };
        this.stop();
        if (!this.#forceStop) { // play if audio end not trigger
            this.#readData();
        }
    }

    playPre() {
        if (this.#playlist.length === 0) {
            return;
        }
        if (this.#currentOrderNum === 0) {
            this.#currentOrderNum = this.#playlist.length - 1;
        } else {
            this.#currentOrderNum--;
        };
        this.play();
    }

    playNext() {
        if (this.#playlist.length === 0) {
            return;
        }
        if (this.#currentOrderNum === this.#playlist.length - 1) {
            this.#currentOrderNum = 0;
        } else {
            this.#currentOrderNum++;
        };
        this.play();
    }

    stop() {
        if (this.#source) {
            this.#forceStop = true;
            this.#source.stop();
            cancelAnimationFrame(this.#animationId); //stop the animation
            this.#updateTitle(this.#APP_NAME);
            this.#mirror.getContext('2d').clearRect(0, 0, this.#mirror.width, this.#mirror.height);
            this.#drawMarkCat();
        }
    }
    
    #drawMarkCat() {
        var that = this,
            imgPath = 'resource/img/mmplayer_mosaik.png',
            img = new Image();
        img.onload = function() {
            var c = that.#canvas,
                ctx = that.#canvas.getContext('2d'),
                w = c.width,
                h = c.height;
            ctx.clearRect(0, 0, w, h);
            ctx.drawImage(img, 0, 0, w, h);
        }
        img.onerror = function(e) {
            console.log(e);
        }
        img.src = imgPath;
    }

    #addVolumeListener() { // rely on ui: volumeValueId && volumeCtrlId
        volumeValueId.value = volumeCtrlId.value;
        let that = this;
        volumeCtrlId.addEventListener("input", function(e){
            volumeValueId.value = volumeCtrlId.value;
            if(that.#gainCtrl){
                that.#gainCtrl.gain.setValueAtTime(parseInt(volumeCtrlId.value)/100, 0);
                that.#playlist[that.#currentOrderNum].volume = volumeCtrlId.value;
                that.#savePlayList();
            }
        });
    }

    #updateVolume() { // rely on ui: volumeValueId && volumeCtrlId
        if (this.#gainCtrl) {
            volumeCtrlId.value = this.#playlist[this.#currentOrderNum].volume;
            volumeValueId.value = volumeCtrlId.value;
            this.#gainCtrl.gain.setValueAtTime(parseInt(volumeCtrlId.value)/100, 0);
        }
    }

    #updateTitle(text) {
        this.#infoBar.innerText = text;
    }

    #readData() {
        var that = this;
        let song = this.#playlist[this.#currentOrderNum];
        if (song.file == null) {
            // already in db
            let result = DB.getInstance().get(song.hash);
            result.then(function(item) {
                that.#decodeData(item.data);
            });
        } else {
            // is a file, try insert to db
            var reader = new FileReader();
            reader.onload = function(e) {
                var data = e.target.result;
                var md5 = SparkMD5.ArrayBuffer.hash(data);
                console.info(md5);
                song.hash = md5;
                var db_song = {};
                db_song.name = song.name;
                db_song.hash = song.hash;
                db_song.data = data;
                DB.getInstance().insert(db_song).then(function(){
                    song.file = null; // inserted, clear
                    that.#savePlayList();
                });
                that.#decodeData(data);
            };
            reader.onerror = function(e) {
                that.#updateTitle('!Fail to read the file');
                console.error(e);
            };
            reader.readAsArrayBuffer(song.file);
        }
    }

    #audioEnd() {
        console.info("audio end");
        this.#source = null;
        this.#updateTitle(this.#APP_NAME);
        if (this.#forceStop) {
            this.#forceStop = false;
            this.play();
        } else {
            this.playNext();
        }
    }

    #drawSpectrum(audioCtx, buffer) {
        var that = this,
            source = audioCtx.createBufferSource(),
            analyser = audioCtx.createAnalyser(),
            canvas = this.#canvas,
            mirror = this.#mirror,
            canvasCtx = canvas.getContext('2d'),
            mirrorCtx = mirror.getContext('2d'),
            cwidth = canvas.width,
            cheight = canvas.height - 2,
            meterWidth = 10, //width of the meters in the spectrum
            capHeight = 2,
            capStyle = '#fff',
            meterNum = 800 / (10 + 2), //count of the meters
            capYPositionArray = [], ////store the vertical position of hte caps for the preivous frame
            gradient = canvasCtx.createLinearGradient(0, 0, 0, 300);
        gradient.addColorStop(1, '#0f0');
        gradient.addColorStop(0.5, '#ff0');
        gradient.addColorStop(0, '#f00');
        //setup the audio
        source.buffer = buffer;
        console.info("duratio", buffer);
        this.#gainCtrl = audioCtx.createGain();
        source.connect(this.#gainCtrl);
        this.#gainCtrl.connect(analyser);
        analyser.connect(audioCtx.destination);
        cancelAnimationFrame(this.#animationId); //stop the previous animation
        source.onended = function() {
            that.#audioEnd();
        };
        source.start();
        this.#updateVolume();
        this.#updateTitle('Playing ' + this.#playlist[this.#currentOrderNum].name.replace(/\.[^/.]+$/, ""));
        this.#source = source;
        var drawFrame = function() {
            var array = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(array);
            //draw the visualizer stuff on the canvas
            var step = Math.round(array.length / meterNum); //sample limited data from the total array
            canvasCtx.clearRect(0, 0, cwidth, cheight);
            for (var i = 0; i < meterNum; i++) {
                var value = array[i * step];
                if (capYPositionArray.length < Math.round(meterNum)) {
                    capYPositionArray.push(value);
                };
                canvasCtx.fillStyle = capStyle;
                //draw the cap, with transition effect
                if (value < capYPositionArray[i]) {
                    canvasCtx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
                } else {
                    canvasCtx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
                    capYPositionArray[i] = value;
                };
                canvasCtx.fillStyle = gradient; //set the filllStyle to gradient for a better look
                canvasCtx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter
            }
            //draw the mirror
            mirrorCtx.clearRect(0, 0, cwidth, cheight);
            mirrorCtx.drawImage(canvas, 0, -100, cwidth, cheight);
            that.#animationId = requestAnimationFrame(drawFrame);
        };
        that.#animationId = requestAnimationFrame(drawFrame);
    }

    #decodeData(arraybuffer) {
        var that = this,
            audioContext = this.#audioContext;
        if (audioContext === null) {
            return;
        };
        var currentFileName = this.#playlist[this.#currentOrderNum].name.replace(/\.[^/.]+$/, "");
        this.#updateTitle('Decoding ' + currentFileName);
        //disable the control to avoid errors
        this.#disableControl();
        audioContext.decodeAudioData(arraybuffer, function(buffer) {
            that.#enableControl();
            that.#updateTitle('Decode succussfully,start the visualizer');
            that.#drawSpectrum(audioContext, buffer);
        }, function(e) {
            that.#enableControl();
            that.#updateTitle('!Fail to decode the file');
            console.log(e);
            setTimeout(() => {that.playNext()}, 3000);
        });
    }

    #savePlayList() {
        var save_list = [];
        for (var song of this.#playlist) {
            if (song.file == null) {
                save_list.push(song);
            }
        }
        localStorage["playlist"] = JSON.stringify(save_list);
    }
}